"""
可定制的会议邀请邮件发送器。

使用示例：
    python send_invites.py --recipients data/recipients.csv \
        --body-template templates/body_template.txt \
        --subject-template templates/subject_template.txt

依赖组件：
    - pandas（用于读取CSV/Excel文件）
    - Python 内置的 smtplib / email 模块（用于邮件发送）
    - string.Formatter（用于占位符替换）

重要提示：
    - 使用前请先配置 SMTP_CONFIG 字典中的邮箱服务器信息
    - 务必使用邮箱服务商提供的 SMTP 授权码，切勿在代码中写入明文登录密码
    - 可将授权码设置为环境变量 SMTP_AUTH_CODE，避免在代码中硬编码
"""

from __future__ import annotations

import argparse
import os
import smtplib
import ssl
from dataclasses import dataclass
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from pathlib import Path
from string import Formatter
from typing import Dict, Iterable, List, Tuple

import pandas as pd


# ==================== SMTP 服务器配置 ====================
# 使用前请根据你的邮箱服务商修改以下配置：
# - host: SMTP服务器地址（如smtp.qq.com, smtp.163.com等）
# - port: SMTP服务器端口（通常SSL为465，STARTTLS为587）
# - use_ssl: 是否使用SSL加密连接
# - use_starttls: 是否使用STARTTLS加密
# - sender_email: 发件人邮箱地址
# - auth_code: SMTP授权码（建议通过环境变量SMTP_AUTH_CODE设置）
# - display_name: 发件人显示名称
SMTP_CONFIG = {
    "host": "smtp.example.com",        # 改为你的SMTP服务器
    "port": 465,                       # 改为你的SMTP端口
    "use_ssl": True,                   # 根据你的邮箱服务商选择加密方式
    "use_starttls": False,             # 通常use_ssl和use_starttls二选一
    "sender_email": "organizer@example.com",  # 改为你的发件邮箱
    "auth_code": os.environ.get("SMTP_AUTH_CODE", "CHANGE_ME"),  # 从环境变量获取或直接填写
    "display_name": "会议组织者",       # 发件人显示名称
}


class RecipientLoadError(RuntimeError):
    """当收件人文件无法解析时抛出的自定义异常"""
    pass


def parse_args() -> argparse.Namespace:
    """
    解析命令行参数
    
    返回:
        argparse.Namespace: 包含所有命令行参数的对象
    """
    parser = argparse.ArgumentParser(
        description="根据模板个性化发送会议邀请邮件。"
    )
    parser.add_argument(
        "--recipients",
        required=True,
        help="收件人列表路径（支持 CSV、XLS、XLSX 格式）。",
    )
    parser.add_argument(
        "--body-template",
        required=True,
        help="邮件正文模板文件路径（纯文本），支持 {name}、{email} 等占位符。",
    )
    parser.add_argument(
        "--subject-template",
        required=True,
        help="邮件主题模板文件路径（纯文本），支持 {name}、{email} 等占位符。",
    )
    parser.add_argument(
        "--email-column",
        default="email",
        help="收件人邮箱字段名称，默认为 'email'。如果CSV中列名不同，请指定。",
    )
    parser.add_argument(
        "--name-column",
        default="name",
        help="收件人姓名字段名称，默认为 'name'。如果CSV中列名不同，请指定。",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="试运行模式：仅渲染模板并输出日志，不真正发送邮件。用于测试模板是否正确。",
    )
    return parser.parse_args()


@dataclass
class Recipient:
    """收件人数据类，存储单个收件人的信息"""
    name: str           # 收件人姓名
    email: str          # 收件人邮箱
    data: Dict[str, str]  # 所有字段的字典，用于模板替换


def load_recipients(path: Path, email_column: str, name_column: str) -> List[Recipient]:
    """
    从CSV或Excel文件加载收件人列表
    
    参数:
        path: 收件人文件路径
        email_column: 邮箱字段的列名
        name_column: 姓名字段的列名
        
    返回:
        List[Recipient]: 收件人对象列表
        
    异常:
        RecipientLoadError: 当文件不存在、格式不支持或缺少必需列时抛出
    """
    if not path.exists():
        raise RecipientLoadError(f"未找到收件人文件：{path}")

    # 根据文件后缀判断文件类型
    suffix = path.suffix.lower()
    try:
        if suffix == ".csv":
            frame = pd.read_csv(path)          # 读取CSV文件
        elif suffix in {".xls", ".xlsx"}:
            frame = pd.read_excel(path)        # 读取Excel文件
        else:
            raise RecipientLoadError(
                f"不支持的收件人文件类型「{suffix}」，请使用 CSV 或 Excel 格式。"
            )
    except Exception as exc:
        raise RecipientLoadError(f"加载收件人文件失败：{exc}") from exc

    # 检查必需的列是否存在
    missing_columns = [col for col in (email_column, name_column) if col not in frame.columns]
    if missing_columns:
        raise RecipientLoadError(
            f"缺少必需列：{', '.join(missing_columns)}。"
            "请确认表头是否正确，或使用 --email-column 和 --name-column 参数指定列名。"
        )

    # 将数据框转换为收件人对象列表
    recipients: List[Recipient] = []
    for _, row in frame.iterrows():
        email = str(row[email_column]).strip()  # 获取邮箱并去除首尾空格
        name = str(row[name_column]).strip()    # 获取姓名并去除首尾空格
        
        # 跳过邮箱为空的行
        if not email or email.lower() == "nan":
            print(f"[警告] 已跳过缺少邮箱的行：{row.to_dict()}")
            continue
            
        # 跳过姓名为空的行
        if not name or name.lower() == "nan":
            print(f"[警告] 因姓名为空，已跳过 {email}。")
            continue
            
        # 将整行数据转换为字典，用于模板变量替换
        # 处理空值：将NaN转换为空字符串
        record = {
            k: ("" if pd.isna(v) else str(v).strip())
            for k, v in row.items()
        }
        recipients.append(Recipient(name=name, email=email, data=record))
        
    return recipients


def load_template(path: Path) -> str:
    """
    加载模板文件内容
    
    参数:
        path: 模板文件路径
        
    返回:
        str: 模板文件内容
        
    异常:
        FileNotFoundError: 当模板文件不存在时抛出
        OSError: 当读取文件失败时抛出
    """
    try:
        content = path.read_text(encoding="utf-8")  # 以UTF-8编码读取文件
    except FileNotFoundError:
        raise FileNotFoundError(f"未找到模板文件：{path}") from None
    except OSError as exc:
        raise OSError(f"读取模板文件 {path} 失败：{exc}") from exc
    return content


class StrictFormatter(Formatter):
    """
    严格的字符串格式化器，继承自string.Formatter
    
    特性：
    - 当模板中存在但数据中不存在的占位符时，抛出明确的KeyError
    - 便于调试模板变量缺失问题
    """

    def get_value(self, key, args, kwargs):  # type: ignore[override]
        """
        重写父类方法，实现严格的变量检查
        
        参数:
            key: 占位符键名
            args: 位置参数（未使用）
            kwargs: 关键字参数（包含模板变量的字典）
            
        返回:
            对应的变量值
            
        异常:
            KeyError: 当变量不存在时抛出
        """
        if isinstance(key, str):
            if key in kwargs:
                return kwargs[key]  # 返回变量值
            raise KeyError(key)     # 变量不存在，抛出异常
        return super().get_value(key, args, kwargs)


# 创建全局格式化器实例
FORMATTER = StrictFormatter()


def render_template(template_text: str, context: Dict[str, str]) -> str:
    """
    使用给定的上下文数据渲染模板
    
    参数:
        template_text: 模板文本，包含 {variable} 格式的占位符
        context: 变量字典，键为占位符名称，值为要替换的内容
        
    返回:
        str: 渲染后的文本
        
    异常:
        KeyError: 当模板中的占位符在context中不存在时抛出
    """
    try:
        # 使用格式化器替换模板中的占位符
        return FORMATTER.vformat(template_text, (), context)
    except KeyError as exc:
        missing_key = exc.args[0]  # 获取缺失的键名
        raise KeyError(f"模板占位符 {{{missing_key}}} 在收件人数据中缺失。请检查数据文件是否包含该字段。") from exc


def build_email(subject: str, body: str, recipient: Recipient) -> MIMEMultipart:
    """
    构建邮件消息对象
    
    参数:
        subject: 邮件主题（已渲染）
        body: 邮件正文（已渲染）
        recipient: 收件人对象
        
    返回:
        MIMEMultipart: 完整的邮件消息对象
    """
    msg = MIMEMultipart()
    # 设置发件人（包含显示名称）
    msg["From"] = f"{SMTP_CONFIG['display_name']} <{SMTP_CONFIG['sender_email']}>"
    # 设置收件人（包含显示名称）
    msg["To"] = f"{recipient.name} <{recipient.email}>"
    msg["Subject"] = subject  # 设置邮件主题
    # 添加正文部分（纯文本，UTF-8编码）
    msg.attach(MIMEText(body, "plain", "utf-8"))
    return msg


def open_mailer() -> Tuple[smtplib.SMTP, bool]:
    """
    建立SMTP连接并登录邮箱
    
    返回:
        Tuple[smtplib.SMTP, bool]: (SMTP连接对象, 是否已建立TLS加密)
        
    异常:
        RuntimeError: 当SMTP授权码未配置时抛出
    """
    # 获取SMTP配置
    use_ssl = SMTP_CONFIG.get("use_ssl", False)
    use_starttls = SMTP_CONFIG.get("use_starttls", False)
    host = SMTP_CONFIG["host"]
    port = SMTP_CONFIG["port"]
    
    # 根据配置选择合适的连接方式
    if use_ssl:
        # 使用SSL加密连接
        context = ssl.create_default_context()
        server: smtplib.SMTP = smtplib.SMTP_SSL(host, port, context=context)
        established_tls = True  # SSL连接默认已加密
    else:
        # 普通连接
        server = smtplib.SMTP(host, port)
        established_tls = False
        if use_starttls:
            # 启用STARTTLS加密
            server.starttls(context=ssl.create_default_context())
            established_tls = True
    
    # 登录邮箱
    username = SMTP_CONFIG["sender_email"]
    auth_code = SMTP_CONFIG["auth_code"]
    
    # 检查授权码是否已配置
    if not auth_code or auth_code == "CHANGE_ME":
        raise RuntimeError(
            "尚未配置 SMTP 授权码。"
            "请设置环境变量 SMTP_AUTH_CODE 或更新 SMTP_CONFIG 字典中的 auth_code 字段。"
        )
        
    server.login(username, auth_code)
    return server, established_tls


def send_messages(
    recipients: Iterable[Recipient],
    subject_template: str,
    body_template: str,
    dry_run: bool = False,
) -> None:
    """
    批量发送邮件消息
    
    参数:
        recipients: 收件人列表
        subject_template: 主题模板
        body_template: 正文模板
        dry_run: 是否为试运行模式（不实际发送）
        
    异常:
        RuntimeError: 当SMTP连接失败时抛出
    """
    successes = 0      # 成功计数
    failures = 0       # 失败计数
    failed_records: List[Tuple[Recipient, Exception]] = []  # 失败记录

    mailer = None
    # 非试运行模式时建立SMTP连接
    if not dry_run:
        try:
            mailer, _ = open_mailer()
            print("[信息] SMTP 连接已建立。")
        except Exception as exc:
            raise RuntimeError(f"无法建立 SMTP 连接：{exc}") from exc

    # 遍历所有收件人
    for recipient in recipients:
        print(f"[信息] 正在处理 {recipient.name} <{recipient.email}> ...")
        try:
            # 准备模板变量上下文
            context = dict(recipient.data)  # 复制收件人数据
            context.setdefault("organizer_name", SMTP_CONFIG["display_name"])  # 添加组织者名称
            
            # 渲染主题和正文模板
            subject = render_template(subject_template, context)
            body = render_template(body_template, context)
            
            if dry_run:
                # 试运行模式：只显示渲染结果，不发送
                print("       仅预览：")
                print(f"       主题：{subject}")
                preview = "\n       ".join(body.splitlines())  # 格式化预览
                print(f"       正文：\n       {preview}")
            else:
                # 实际发送模式
                message = build_email(subject, body, recipient)
                assert mailer is not None  # 类型检查断言
                mailer.send_message(message)  # 发送邮件
                print("       发送成功。")
            successes += 1
        except Exception as exc:
            # 处理发送失败的情况
            failures += 1
            failed_records.append((recipient, exc))
            print(f"[错误] 发送到 {recipient.email} 失败：{exc}")

    # 关闭SMTP连接
    if mailer is not None:
        mailer.quit()

    # 输出发送结果统计
    print("-" * 60)
    print(f"[结果] 成功：{successes} | 失败：{failures}")
    if failed_records:
        print("[结果] 失败详情：")
        for recipient, error in failed_records:
            print(f"    - {recipient.email}（{recipient.name}）：{error}")


def main() -> None:
        """
        因客户要求，该部分业务逻辑已折叠
        """


if __name__ == "__main__":
    main()